﻿// --------------------------------------------------------------------------------------------------------------------
// <copyright file="With_ModelBase.cs" company="JDML Consulting Ltd.">
//   Copyright 2011 Photon Project - http://photon.codeplex.com
// </copyright>
// --------------------------------------------------------------------------------------------------------------------

using System.Collections.ObjectModel;
using System.ComponentModel.DataAnnotations;
using System.Diagnostics;

namespace Photon.ComponentModel.Modelling
{
    using System;
    using System.Collections.Generic;
    using System.ComponentModel;
    using System.Linq;
    using System.Runtime.Serialization;
    using Microsoft.VisualStudio.TestTools.UnitTesting;
    using Reflection;
    using Serialization.Json;
    using TestAssets;
    using Testing;

    public class With_ModelBase<TModel> where TModel : ModelBase, new()
    {
        public TestContext TestContext { get; set; }

        public TModel Model { get; protected set; }

        [TestInitialize]
        public void InitializeModel()
        {
            Model = new TModel();
        }
    }

    /// <summary>
    /// Base class and container for <see cref="ModelBase"/> tests.
    /// </summary>
    public partial class With_ModelBase
    {
        [TestClass]
        public class When_setting_a_property : With_ModelBase<PropertyChangeTestModel>
        {
            [TestMethod]
            public void Should_detect_changes_and_notify_for_value_types()
            {
                Model.ValueTypeValue = 1;
                Model.AssertPropertyChanged.Raised(() => Model.ValueTypeValue, 1);
                Model.ValueTypeValue = 1;
                Model.AssertPropertyChanged.NotRaised(() => Model.ValueTypeValue);
            }

            [TestMethod]
            public void Should_detect_changes_and_notify_for_nullable_types()
            {
                Model.NullableTypeValue = 1;
                Model.AssertPropertyChanged.Raised(() => Model.NullableTypeValue, 1);
                Model.NullableTypeValue = 1;
                Model.AssertPropertyChanged.IsEmpty();
                Model.NullableTypeValue = null;
                Model.AssertPropertyChanged.Raised(() => Model.NullableTypeValue, null);
                Model.NullableTypeValue = null;
                Model.AssertPropertyChanged.IsEmpty();
            }

            [TestMethod]
            public void Should_detect_changes_and_notify_for_reference_types()
            {
                var obj = new object();
                Model.ReferenceTypeValue = obj;
                Model.AssertPropertyChanged.Raised(() => Model.ReferenceTypeValue, obj);
                Model.ReferenceTypeValue = obj;
                Model.AssertPropertyChanged.IsEmpty();
                Model.ReferenceTypeValue = null;
                Model.AssertPropertyChanged.Raised(() => Model.ReferenceTypeValue, null);
                Model.ReferenceTypeValue = null;
                Model.AssertPropertyChanged.IsEmpty();
            }
        }

        [TestClass]
        public class When_setting_a_property_is_undone : With_ModelBase<PropertyChangeTestModel>
        {
            /// <summary>
            /// Verifies that the model raises a <see cref="INotifyPropertyChanged.PropertyChanged"/> event when a property change is 
            /// undone by an extension.
            /// </summary>
            /// <remarks>
            /// It is important that we raise a property change event when a property change is undone, without this controls tend to assume their 
            /// value is current and don't update themselves.
            /// </remarks>
            [TestMethod]
            public void Should_still_raise_property_changed()
            {
                // initialize model and verify pre-test behavior
                Model.TrimmedValue = "Test";
                Model.AssertPropertyChanged.Raised(() => Model.TrimmedValue);
                Model.AssertPropertyChanged.IsEmpty();

                // write a value that will get reverted (in this through trimming)
                Model.TrimmedValue = "  Test  ";
                Model.AssertPropertyChanged.Raised(() => Model.TrimmedValue);
                Assert.AreEqual("Test", Model.TrimmedValue);
            }
        }

        [TestClass]
        public class When_setting_a_property_in_a_suspend_notify_scope : With_ModelBase<PropertyChangeTestModel>
        {
            [TestMethod]
            public void Should_notify_that_the_property_was_changed_when_the_scope_is_exited()
            {
                using (Model.NewSuspendNotify())
                {
                    using (Model.NewSuspendNotify())
                    {
                        Model.ValueTypeValue = 1;
                        Model.AssertPropertyChanged.IsEmpty();
                    }

                    Model.AssertPropertyChanged.IsEmpty();
                }

                Model.AssertPropertyChanged
                    .Raised(() => Model.ValueTypeValue)
                    .IsEmpty();
            }
        }

        [TestClass]
        public class When_setting_multiple_properties_in_a_suspend_notify_scope : With_ModelBase<PropertyChangeTestModel>
        {
            [TestMethod]
            public void Should_notify_for_all_changed_properties_when_the_scope_is_exited()
            {
                using (Model.NewSuspendNotify())
                {
                    using (Model.NewSuspendNotify())
                    {
                        // set 2 properties to changed values 
                        Model.ValueTypeValue = 1;
                        Model.ReferenceTypeValue = new object();
                        
                        // set 1 property to unchanged value
                        Model.NullableTypeValue = Model.NullableTypeValue;
                        
                        // confirm no change
                        Model.AssertPropertyChanged.IsEmpty();
                    }

                    // confirm no change
                    Model.AssertPropertyChanged.IsEmpty();
                }

                // confirm changes for changed properties
                Model.AssertPropertyChanged
                    .Raised(() => Model.ValueTypeValue)
                    .Raised(() => Model.ReferenceTypeValue)
                    .IsEmpty();
            }
        }

        [TestClass]
        public class When_deserializing : With_ModelBase<When_deserializing.TestModel>
        {
            #region Fields

            /// <summary>
            /// Tracks the number of property change events that have been raised.
            /// </summary>
            public static int PropertyChangeCount;

            #endregion

            [TestInitialize]
            public void Given()
            {
                Model.Value = 1;
                Model.AnotherValue = 1;

                // reset counters
                PropertyChangeCount = 0;
                ValidationTrackingAttribute.ValidationCount = 0;

                // serialize\deserialize
                Model = JsonConverter.Instance.FromText<TestModel>(
                    JsonConverter.Instance.ToText(Model));

                Assert.IsTrue(Model.IsInitialized);
            }

            [TestMethod]
            public void Should_deserialize_property_value()
            {
                Assert.AreEqual(1, Model.Value);
            }

            [TestMethod]
            public void Should_not_raise_property_changed_events()
            {
                Assert.AreEqual(0, PropertyChangeCount);
            }

            [TestMethod]
            public void Should_not_validate()
            {
                Assert.AreEqual(0, ValidationTrackingAttribute.ValidationCount);
            }

            [TestMethod]
            public void Should_not_raise_property_changed_for_all_properties_on_next_property_change()
            {
                Model.AnotherValue = 2;
                Assert.AreEqual(1, PropertyChangeCount);
            }

            /// <summary>
            /// Serializable test model
            /// </summary>
            [DataContract]
            public class TestModel : ModelBase
            {
                #region Fields

                private int _value;
                private int _anotherValue;

                #endregion

                [DataMember, ValidationTracking()]
                public int Value
                {
                    get
                    {
                        return _value;
                    }
                    set
                    {
                        // constructor is not invoked during deserialization, so attach handle here
                        Initialize();
                        SetProperty(ref _value, value, "Value");
                    }
                }

                [DataMember, ValidationTracking()]
                public int AnotherValue
                {
                    get
                    {
                        return _value;
                    }
                    set
                    {
                        // constructor is not invoked during deserialization, so attach handle here
                        Initialize();
                        SetProperty(ref _anotherValue, value, "AnotherValue");
                    }
                }

                private void Initialize()
                {
                    if (!IsInitialized)
                    {
                        PropertyChanged += (s, e) => PropertyChangeCount++;
                        IsInitialized = true;
                    }
                }

                public bool IsInitialized { get; private set; }
            }
        }

        [TestClass]
        public class When_raising_property_changed : With_ModelBase<PropertyChangeTestModel>
        {
            [TestMethod]
            public void If_property_name_is_empty_should_raise_with_empty_property_name_in_args()
            {
                TestRaisePropertyChanged(string.Empty);
            }

            [TestMethod]
            public void If_property_name_is_null_should_raise_with_empty_property_name_in_args()
            {
                TestRaisePropertyChanged(null, string.Empty);
            }

            [TestMethod]
            public void If_property_name_is_valid_should_raise_with_specified_property_name()
            {
                TestRaisePropertyChanged(ExpressionExtensions.ExtractMemberName(() => Model.ValueTypeValue));
            }

            [TestMethod]
            public void If_property_name_is_not_valid_should_throw_key_not_found_exception()
            {
                AssertException.Throws<KeyNotFoundException>(() => TestRaisePropertyChanged("InvalidPropertyName"));
            }

            [TestMethod]
            public void If_property_expression_is_valid_should_raise_with_correct_member_name()
            {
                Model.RaisePropertyChanged(() => Model.ValueTypeValue);
                Model.AssertPropertyChanged.Raised(() => Model.ValueTypeValue);
            }

            [TestMethod]
            public void If_property_expression_is_not_valid_should_throw_key_not_found_exception()
            {
                AssertException.Throws<KeyNotFoundException>(() => Model.RaisePropertyChanged(() => String.Empty));
            }

            private void TestRaisePropertyChanged(string raisePropertyName)
            {
                TestRaisePropertyChanged(raisePropertyName, raisePropertyName);
            }

            private void TestRaisePropertyChanged(string raisePropertyName, string eventArgsPropertyName)
            {
                Model.RaisePropertyChanged(raisePropertyName);
                Model.AssertPropertyChanged.Raised(eventArgsPropertyName);
            }
        }

        [TestClass]
        public class When_raising_property_changed_in_a_suspend_notify_scope : With_ModelBase<PropertyChangeTestModel>
        {
            [TestMethod]
            public void If_property_name_is_empty_should_not_raise_until_scope_is_exited()
            {
                TestRaisePropertyChangedInSuspendNotifyScope(string.Empty);
            }

            [TestMethod]
            public void If_property_name_is_null_should_not_raise_until_scope_is_exited()
            {
                TestRaisePropertyChangedInSuspendNotifyScope(null, string.Empty);
            }

            [TestMethod]
            public void If_property_name_is_valid_should_not_raise_until_scope_is_exited()
            {
                TestRaisePropertyChangedInSuspendNotifyScope(ExpressionExtensions.ExtractMemberName(() => Model.ValueTypeValue));
            }

            private void TestRaisePropertyChangedInSuspendNotifyScope(string raisePropertyName)
            {
                TestRaisePropertyChangedInSuspendNotifyScope(raisePropertyName, raisePropertyName);
            }

            private void TestRaisePropertyChangedInSuspendNotifyScope(string raisePropertyName, string eventArgsPropertyName)
            {
                using (Model.NewSuspendNotify())
                {
                    using (Model.NewSuspendNotify())
                    {
                        Model.RaisePropertyChanged(raisePropertyName);
                        Model.AssertPropertyChanged.IsEmpty();
                    }

                    Model.AssertPropertyChanged.IsEmpty();
                }

                Model.AssertPropertyChanged.Raised(eventArgsPropertyName);
            }
        }

        [TestClass]
        public class When_created_with_invalid_property_value : With_ModelBase<ValidationTestModel>
        {
            [TestMethod]
            public void Should_be_initialized_with_no_errors()
            {
                Assert.IsFalse(Model.HasErrors);
                AssertDataErrorInfo.NotHasErrors(Model);
                AssertDataErrorInfo.NotHasErrors(Model, () => Model.RequiredValue);
                AssertDataErrorInfo.NotHasErrors(Model, () => Model.MutuallyExclusiveValueX);
                AssertDataErrorInfo.NotHasErrors(Model, () => Model.MutuallyExclusiveValueY);
            }
        }

        [TestClass]
        public class When_a_setting_a_property_that_has_attribute_validation_to_an_invalid_value : With_ModelBase
        {
            private ValidationTestModel SystemUnderTest { get; set; }

            [TestInitialize]
            public void TestInitialize()
            {
                SystemUnderTest = new ValidationTestModel();
            }

            /// <summary>
            /// We bypass certain aspects of validation so this needs to be tested (there was previously a issue)
            /// </summary>
            [TestMethod]
            public void Should_use_display_name_in_validation_messages()
            {
                // initialize the model and invalidate it 
                SystemUnderTest.Validate();

                // get the eror and assert we have picked up the custom display name
                var errors = SystemUnderTest.GetErrors("RequiredValue").OfType<string>().ToArray();
                Assert.AreEqual(1, errors.Count());
                Assert.IsTrue(errors.First().Contains("Custom Display Name"));
            }
        }

        [TestClass]
        public class When_a_viewmodel_has_a_property_with_the_same_name_as_anccestor_class : With_ModelBase
        {
            private ViewModelWhichHasSamePropertyNameAsParentType<string> SystemUnderTest { get; set; }

            [TestInitialize]
            public void TestInitialize()
            {
                SystemUnderTest = new ViewModelWhichHasSamePropertyNameAsParentType<string>();    
            }

            [TestMethod]
            public void Should_raise_property_changed_when_setting_specialisation()
            {
                SystemUnderTest.Something = "gear";
                SystemUnderTest.AssertPropertyChanged.Raised(() => SystemUnderTest.Something, "gear");
            }

            [TestMethod]
            public void Should_rasie_property_changed_when_setting_base_property()
            {
                ((ViewModelWithAPropertyToOverride)SystemUnderTest).Something = "cheese";

                SystemUnderTest.AssertPropertyChanged.Raised(() => SystemUnderTest.Something, "cheese");
            }

            [TestMethod]
            public void Property_info_should_be_for_property_lowest_in_inheritence_tree()
            {
                var property = SystemUnderTest.MetaModelType.Properties["Something"];
            }
        }

#if !SILVERLIGHT

        /// <summary>
        /// Basic performance testing
        /// </summary>
        /// <remarks>
        /// Basic performance tests, used to verify the overhead of using the model classes, and as an entry point 
        /// for tuning with a profiler.
        /// </remarks>
        [TestClass]
        public class When_comparing_performance_to_hand_written_code : With_ModelBase
        {
            protected PhotonPerformanceTestModel PhotonModel { get; private set; }

            protected NativePerformanceTestModel NativeModel { get; private set; }

            [TestInitialize]
            public void TestInitialize()
            {
                PhotonModel = new PhotonPerformanceTestModel();
                NativeModel = new NativePerformanceTestModel();
            }
            
            [TestMethod]
            public void If_getting_and_setting_properties_should_not_incur_too_much_overhead()
            {
                const int Iterations = 100000;

                TestPerformance(model =>
                    {
                        for (var i = 0; i < Iterations; i++)
                        {
                            model.Value = i;
                        }
                    });
            }

            [TestMethod]
            public void If_notifying_of_property_changes_should_not_incur_too_much_overhead()
            {
                const int Iterations = 10000;

                int[] propertyChanged = { 0 };

                PhotonModel.PropertyChanged += (s, e) => propertyChanged[0]++;
                NativeModel.PropertyChanged += (s, e) => propertyChanged[0]++;

                TestPerformance(model =>
                    {
                        for (var i = 0; i < Iterations; i++)
                        {
                            model.Value = i;
                        }
                    });
            }

            [TestMethod]
            public void If_validating_should_not_incur_too_much_overhead()
            {
                const int Iterations = 100000;

                TestPerformance(model =>
                    {
                        for (var i = 0; i < Iterations; i++)
                        {
                            PhotonModel.ValueWithValidation = i;
                        }
                    });
            }

            public void TestPerformance(Action<IPerformanceModel> action)
            {
                var timings = new List<long>();
                for (var i = 0; i < 10; i++)
                {
                    var elapsed1 = TimeOperation(PhotonModel, action);
                    var elapsed2 = TimeOperation(NativeModel, action);
                    
                    // discard first result as I believe its affected by JITTING
                    if (i != 0)
                    {
                        timings.Add(elapsed1 / elapsed2);
                    }
                }

                var average = timings.Average();
                Console.WriteLine(@"Performance is {0} times slower.", average);

                Assert.IsTrue(average < 25, "Performance is degrading");
            }

            public long TimeOperation(IPerformanceModel model, Action<IPerformanceModel> action)
            {
                var stopwatch = new Stopwatch();
                stopwatch.Start();
                action(model);
                stopwatch.Stop();
                return stopwatch.ElapsedTicks;
            }

            public interface IPerformanceModel
            {
                int Value { get; set; }
                int ValueWithValidation { get; set; }
            }

            #region Nested type: PerformanceModel

            public class PhotonPerformanceTestModel : ModelBase, IPerformanceModel
            {
                private int _value;
                private int _valueWithValidation;

                public int Value
                {
                    get { return _value; }
                    set { SetProperty(ref _value, value, "Value"); }
                }

                [Range(0, 100)]
                public int ValueWithValidation
                {
                    get { return _valueWithValidation; }
                    set { SetProperty(ref _valueWithValidation, value, "ValueWithValidation"); }
                }
            }

            #endregion

            #region Nested type: PerformanceModelRaw

            public class NativePerformanceTestModel : INotifyPropertyChanged, IDataErrorInfo, IPerformanceModel
            {
                private int _value;
                private readonly Dictionary<string, string> _errors = new Dictionary<string, string>();
                private int _valueWithValidation;

                public int Value
                {
                    get
                    {
                        return _value;
                    }
                    set
                    {
                        if (_value != value)
                        {
                            _value = value;
                            RaisePropertyChanged("Value");
                        }
                    }
                }

                [Range(0, 100)]
                public int ValueWithValidation
                {
                    get
                    {
                        return _valueWithValidation;
                    }
                    set
                    {
                        if (_valueWithValidation != value)
                        {
                            _valueWithValidation = value;
                            _errors.Remove("ValueWithValidation");
                            var validationResults = new Collection<ValidationResult>();
                            if (!Validator.TryValidateProperty(value, new ValidationContext(this, null, null) { MemberName = "ValueWithValidation" },
                                validationResults))
                            {
                                _errors.Add("ValueWithValidation", validationResults[0].ErrorMessage);
                            }
                            RaisePropertyChanged("Value");
                        }
                    }
                }

                #region INotifyPropertyChanged Members

                public event PropertyChangedEventHandler PropertyChanged;

                #endregion

                private void RaisePropertyChanged(string propertyName)
                {
                    if (PropertyChanged != null)
                    {
                        PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
                    }
                }

                public string this[string columnName]
                {
                    get
                    {
                        string result;
                        _errors.TryGetValue(columnName, out result);
                        return result;
                    }
                }

                public string Error
                {
                    get { throw new NotImplementedException(); }
                }
            }

            #endregion
        }
#endif
    }
}